package editor.widgets;

import editor.ErrorHandler;
import editor.Images;
import editor.PwnIDE;
import editor.code.LineField;
import editor.headers.Header;
import event.ListAddEvent;
import event.ListMoveEvent;
import event.ListRemoveEvent;
import model.caret.Caret;
import model.caret.CaretRef;
import model.caret.TextCaret;
import model.code.CodeFileChunk;
import model.code.Rowable;
import model.code.line.LineList;
import model.code.line.SingleLine;
import observer.ListObserver;
import observer.ObservedList;
import observer.ObservedString;
import util.ArrayUtil;
import widgets.CustomButton;
import widgets.ImageButton;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.security.InvalidParameterException;
import java.util.ArrayList;

/**
 * This is a table for adding rows of {@link ObservedStringField}s for editing {@link ObservedString}s.
 *
 * It is useful for things with multiple columns, like function parameters, where each parameter has a name, type,
 * and description, but it can also be used for things for which there is only one column, like author lists.
 *
 * @author Neil Dickson
 */
public class ObservedStringTable<E extends Rowable> extends JComponent implements ListObserver<E>, ActionListener, AncestorListener {
	private	final CodeFileChunk						parentCode;
	private	CaretRef								caretRef;
	private	final E									dummy;
	private	final int								defaultCopyIndex;

	private final int								nCols;
	private int										nRows;

	private final ArrayList<RowPanel>				rows;
	private	final ObservedList<E>					elements;

	private final String[]							defaults;

	private final JPanel							titlePanel;
	private final JTextField						title;
	private final CustomButton						add;

	private final SingleLine.LineObserver			cellObserver;

	public static final Image[]	ADD_ICONS		= new Image[]{
			Images.ROW_ADD_UP,
			Images.ROW_ADD_HOVER,
			Images.ROW_ADD_DOWN,
			Images.ROW_ADD_DISABLED
	};
	public static final Image[]	REMOVE_ICONS	= new Image[]{
			Images.ROW_REMOVE_UP,
			Images.ROW_REMOVE_HOVER,
			Images.ROW_REMOVE_DOWN,
			Images.ROW_REMOVE_DISABLED
	};
	public static final Image[]	UP_ICONS		= new Image[]{
			Images.ROW_UP_UP,
			Images.ROW_UP_HOVER,
			Images.ROW_UP_DOWN,
			Images.ROW_UP_DISABLED
	};
	public static final Image[]	DOWN_ICONS	= new Image[]{
			Images.ROW_DOWN_UP,
			Images.ROW_DOWN_HOVER,
			Images.ROW_DOWN_DOWN,
			Images.ROW_DOWN_DISABLED
	};
	public static final Border TEXT_FIELD_BORDER = new LineBorder(Color.LIGHT_GRAY);

	/**
	 * Creates an {@link ObservedStringTable} from an array of columns
	 * @param parentCode the code chunk containing the data represented by this ObservedStringTable
	 * @param title the table's title
	 * @param elements an {@link ObservedList} whose elements are the {@link model.code.Rowable}s to be shown in each row
	 * @param dummy a dummy element from which to create new elements for the list
	 * @param defaults the default values for new rows
	 */
	public ObservedStringTable(CodeFileChunk parentCode,String title,ObservedList<E> elements,E dummy,String[] defaults) {
		this(parentCode,title,elements,dummy,defaults,-1);
	}
	public ObservedStringTable(CodeFileChunk parentCode,String title,ObservedList<E> elements,E dummy,String[] defaults,int defaultCopyIndex) {
		this(parentCode,title,elements,dummy,defaults,defaultCopyIndex,null);
	}
	public ObservedStringTable(CodeFileChunk parentCode,String title,ObservedList<E> elements,E dummy,String[] defaults,int defaultCopyIndex,CaretRef caretRef) {
		this.parentCode = parentCode;
		this.dummy = dummy;
		this.defaultCopyIndex = defaultCopyIndex;
		this.caretRef =  caretRef;
//		setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
		titlePanel = new JPanel(null) {	//new FlowLayout(FlowLayout.LEFT,8,0)){
			public Insets getInsets() {
				return new Insets(0,0,0,0);
			}
			public Insets getInsets(Insets insets) {
				insets.bottom = 0;
				insets.top = 0;
				insets.right = 0;
				insets.left = 0;
				return insets;
			}
		};
		if (title!=null && title.length()>0) {
			this.title = new Header.TextLabel(title,Header.TRANSPARENT_COLOUR);
			titlePanel.add(this.title);
		}
		else {
			this.title = null;
		}
		add = new ImageButton(ADD_ICONS[0],ADD_ICONS[1],ADD_ICONS[2],ADD_ICONS[3]);
		add.addActionListener(this);
		if (nRows==0) {
			titlePanel.add(add);
		}
		add(titlePanel);
		this.elements = elements;
		nCols = dummy.getSize();
		nRows = elements.size();
		if (nCols<1) {
			throw new InvalidParameterException("ObservedStringTables must have at least 1 column.");
		}
		this.defaults = defaults;
		// Add this as a listener of the list
		elements.addObserver(this);
		// and observe the length of each cell too, for layout updating.
		cellObserver = new SingleLine.LineObserver() {
			public void valueChanged(SingleLine line, String oldValue) {
				if (line.length()!=oldValue.length()) {
					doLayout();
				}
			}
		};
		for (int row=0;row<nRows;++row) {
			for (int col = 0; col < nCols; ++col) {
				elements.get(row).getStrings()[col].addObserver(cellObserver);
			}
		}

		rows = new ArrayList<RowPanel>(nRows);
		for (int i=0;i<nRows;++i) {
			rows.add(new RowPanel(i));
			add(rows.get(i));
		}
		doLayout();
		addAncestorListener(this);
	}

	/**
	 * Creates an {@link ObservedStringTable} from an {@link ObservedList} of elements.
	 * @param parentCode the code chunk containing the data represented by this ObservedStringTable
	 * @param title the table's title
	 * @param elements an {@link ObservedList} whose elements are the {@link Rowable}s to be shown in each row
	 * @param dummy a dummy element from which to create new elements for the list
	 */
	public ObservedStringTable(CodeFileChunk parentCode,String title,ObservedList<E> elements,E dummy) {
		this(parentCode,title,elements,dummy,null);
	}

	/**
	 * Creates an {@link ObservedStringTable} from an {@link ObservedList} of elements.
	 * @param caretRef the CaretRef controlling the Carets used by this table
	 * @param title the table's title
	 * @param elements an {@link ObservedList} whose elements are the {@link Rowable}s to be shown in each row
	 * @param dummy a dummy element from which to create new elements for the list
	 */
	public ObservedStringTable(CaretRef caretRef,String title,ObservedList<E> elements,E dummy) {
		this(null,title,elements,dummy,null,-1,caretRef);
	}

	/**
	 * @param row the row from which to get the cell
	 * @param col to column from which to get the cell
	 * @return the LineField at the specified row and column, if that location exists, else null
	 */
	public LineField getCell(int row,int col) {
		if (row>=0 && row<nRows && col>=0 && col<rows.get(row).fields.length) {
			return rows.get(row).fields[col];
		}
		return null;
	}

	/**
	 * @return the current number of rows
	 */
	public int getNumRows() {
		return nRows;
	}

	/**
	 * @return the list of {@link Rowable}s in this table
	 */
	public ObservedList<E> getElements() {
		return elements;
	}

	/**
	 * @return the CodeFileChunk of which this table represents a part
	 */
	public CodeFileChunk getParentCode() {
		return parentCode;
	}

	/**
	 * @return the CaretRef of the file this is in, or if not in a file, the CaretRef given upon construction
	 */
	public CaretRef getCaretRef() {
		return (parentCode!=null && parentCode.getFile()!=null) ? parentCode.getFile().getCaretRef() : caretRef;
	}

	/**
	 * Adds a row at the specified position with default values.
	 * @param row index of the row at which to insert the new row
	 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
	 */
	public void addRow(int row,boolean updateList) {
		// Ensure that 0<=row<=nRows
		row = Math.min(nRows,Math.max(0,row));
		if (updateList) {
			// Add the new element
			if (defaults==null) {
				elements.add(row,(E)dummy.copy(parentCode,row),this);
			}
			else {
				String[] tempDefaults = defaults;
				if (row>0 && defaultCopyIndex>=0) {
					tempDefaults = new String[defaults.length];
					for (int i=0;i<defaults.length;++i) {
						tempDefaults[i] = (i!=defaultCopyIndex) ? defaults[i] : elements.get(row-1).getStrings()[i].toString();
					}
				}
				elements.add(row,(E)dummy.copy(parentCode,row,tempDefaults),this);
			}
			PwnIDE.editor.eventDone(new ListAddEvent<E>(elements,row));
		}
		// Add observers on each cell to check for size changes
		for (int col = 0; col < nCols; ++col) {
			elements.get(row).getStrings()[col].addObserver(cellObserver);
		}
		// Add the new RowPanel at the correct position
		rows.add(row,new RowPanel(row));
		rows.get(row).setBackground(getBackground());
		rows.get(row).setForeground(getForeground());
		rows.get(row).setFont(getFont());
		add(rows.get(row),row+1);	// 1 extra due to title
		++nRows;
		// Update all rows to make sure row number set correctly and button states updated, including first on adding a second
		for (int i=0;i<nRows;++i) {
			rows.get(i).updateRow(i);
			elements.get(i).setLocation(parentCode,i);
		}
		if (nRows==1) {
			titlePanel.remove(add);
		}
		doLayout();
		CaretRef caretRef = getCaretRef();
		caretRef.set(new TextCaret(caretRef,rows.get(row).getField(0).getLines(),0,0,0));
		rows.get(row).getField(0).requestFocus();
	}

	/**
	 * Removes a row at the specified
	 * @param row index of the row to remove
	 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
	 */
	public void removeRow(int row,boolean updateList) {
		// Ensure that 0<=row<nRows
		if (row>=0 && row<nRows) {
			// First, remove the RowPanel
			remove(rows.get(row));
			rows.remove(row);
			--nRows;
			if (updateList) {
				// Remove observers from each cell
				for (int col = 0; col < nCols; ++col) {
					elements.get(row).getStrings()[col].removeObserver(cellObserver);
				}
				// Then remove the element
				E element = elements.remove(row,this);
				Caret caret = getCaretRef().get();
				if (caret!=null && caret instanceof TextCaret) {
					LineList lines = ((TextCaret)caret).getLineList();
					if (ArrayUtil.containsExact(element.getStrings(),lines)) {
						int newRow = (row<nRows) ? row : row-1;
						if (nRows>0) {
							((TextCaret)caret).set(rows.get(newRow).getField(0).getLines(),0,0,0,0,0);
							rows.get(newRow).getField(0).requestFocus();
						}
					}
				}
				PwnIDE.editor.eventDone(new ListRemoveEvent<E>(elements,element,row));
			}
			// Update all rows to make sure row number set correctly and button states updated, including first on removing a second
			for (int i=0;i<nRows;++i) {
				rows.get(i).updateRow(i);
				elements.get(i).setLocation(parentCode,i);
			}
			if (nRows==0) {
				titlePanel.add(add);
			}
			doLayout();
		}
		else {
			ErrorHandler.showMessage("Sorry, for some unknown reason, the program is trying to remove a row with index "+row+" when there are "+nRows+" rows.  Hopefully this bug gets fixed in a later version.");
		}
	}

	/**
	 * Moves a row upward one position
	 * @param row index of the row to move upward
	 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
	 */
	public void moveRowUp(int row,boolean updateList) {
		moveRow(row,row-1,updateList);
	}

	/**
	 * Moves a row downward one position
	 * @param row index of the row to move downward
	 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
	 */
	public void moveRowDown(int row,boolean updateList) {
		moveRow(row,row+1,updateList);
	}

	/**
	 * Moves a row from one position to another
	 * @param from current index of the row to move
	 * @param to new index to which to move the row
	 * @param updateList true indicates that the list must be updated, i.e. that this widget caused the event
	 */
	protected void moveRow(int from,int to,boolean updateList) {
		if (from>=0 && from<nRows && to>=0 && to<nRows) {
			if (updateList) {
				// First, move the element
				elements.move(from,to,this);
				PwnIDE.editor.eventDone(new ListMoveEvent<E>(elements,from,to));
			}
			// Then, move the RowPanel within the Container
			remove(rows.get(from));
			add(rows.get(from),to+1);	// 1 extra due to title
			rows.add(to,rows.remove(from));
			for (int i=Math.min(from,to);i<=Math.max(from,to);++i) {
				rows.get(i).updateRow(i);
				elements.get(i).setLocation(parentCode,i);
			}
			doLayout();
		}
		else {
			ErrorHandler.showMessage("Sorry, for some unknown reason, the program is trying to move a row from index "+from+" to index "+to+" when there are "+nRows+" rows.  Hopefully this bug gets fixed in a later version.");
		}
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource()==add) {
			try {
				addRow(0,true);
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while adding/removing/moving a table row.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}

	public void setBackground(Color bg) {
		super.setBackground(bg);
		setOpaque(bg.getAlpha()==255);
		if (title!=null) {
			title.setBackground(bg);
			title.setOpaque(bg.getAlpha()==255);
		}
		titlePanel.setBackground(bg);
		titlePanel.setOpaque(bg.getAlpha()==255);
		add.setBackground(bg);
		if (rows!=null) {
			for (RowPanel row : rows) {
				row.setBackground(bg);
			}
		}
	}

	public void setForeground(Color fg) {
		super.setForeground(fg);
		if (title!=null) {
			title.setForeground(fg);
		}
		titlePanel.setForeground(fg);
		add.setForeground(fg);
		if (rows!=null) {
			for (RowPanel row : rows) {
				row.setForeground(fg);
			}
		}
	}

	public void setFont(Font font) {
		super.setFont(font);
		if (rows!=null) {
			for (RowPanel row : rows) {
				row.setFont(font);
			}
			doLayout();
		}
	}

	public void doLayout() {
		int height = 0;
		int width = 0;
		if (title!=null) {
			FontMetrics font = title.getFontMetrics(title.getFont());
			title.setBounds(0,0,font.stringWidth(title.getText())+1,font.getHeight());	// 1 extra to avoid weird issue with selecting text in JTextField
			height = title.getHeight();
			width = title.getWidth()+4;
		}
		if (rows.size()>0) {
			add.setVisible(false);
		}
		else {
			add.setLocation(width,0);
			add.setVisible(true);
			height = Math.max(height,add.getHeight());
			width += add.getWidth();
		}
		titlePanel.setBounds(0,0,width,height);
		if (nRows>0) {
			FontMetrics font = rows.get(0).fields[0].getFontMetrics(rows.get(0).fields[0].getFont());
			int rowHeight = font.getHeight()+2;	// 2 extra for border thickness
			int x = 0;
			for (int col=0;col<nCols;++col) {
				int maxWidth = 16;	// Minimum of 16 pixels
				for (RowPanel row : rows) {
					maxWidth = Math.max(maxWidth,row.fields[col].getMinimumSize().width+8+2);	// 8 for space, 2 extra for border thickness
				}
				for (RowPanel row : rows) {
					row.fields[col].setBounds(x,0,maxWidth,rowHeight);
				}
				x += maxWidth+1;	// 1 extra for cell spacing
			}
			int addWidth = rows.get(0).add.getWidth()+1;		// 1 extra for spacing
			int removeWidth = rows.get(0).remove.getWidth()+1;	//
			int upWidth = rows.get(0).up.getWidth()+1;			//
			int downWidth = rows.get(0).down.getWidth();		// no extra, because at end
			for (RowPanel row : rows) {
				row.add.setLocation(x,0);
				row.remove.setLocation(x+addWidth,0);
				row.up.setLocation(x+addWidth+removeWidth,0);
				row.down.setLocation(x+addWidth+removeWidth+upWidth,0);
			}
			width = Math.max(width,x+addWidth+removeWidth+upWidth+downWidth);
			for (RowPanel row : rows) {
				row.setBounds(0,height,width,rowHeight);
				height += rowHeight+1;	// 1 extra for cell spacing
			}
			--height;					// subtract the final cell spacing, since at end
		}
		Dimension dim = new Dimension(width,height);
		setSize(dim);
		setMinimumSize(dim);
		setPreferredSize(dim);
		// For some reason, providing Short.MAX_VALUE indicates that it can be any width, but left-aligns it in a managed layout... *sigh*
		setMaximumSize(new Dimension(Short.MAX_VALUE,height));
	}

	public void listElementAdded(ObservedList<E> s, int index) {
		addRow(index,false);
	}

	public void listElementRemoved(ObservedList<E> s, int index, E element) {
		// Remove observers from each cell
		for (int col = 0; col < nCols; ++col) {
			element.getStrings()[col].removeObserver(cellObserver);
		}
		removeRow(index,false);
	}

	public void listElementSet(ObservedList<E> s, int index, E oldElement) {
		ErrorHandler.showNotImplementedMessage("Sorry, there's an unimplemented function named ObservedStringTable.listElementSet, and it was called, but it shouldn't have been.  Hopefully this bug gets fixed in a later version.");
	}

	public void listElementMoved(ObservedList<E> s, int from, int to) {
		moveRow(from,to,false);
	}

	public void ancestorAdded(AncestorEvent event) {
		elements.addObserver(this);
		for (int row=0;row<nRows;++row) {
			for (int col = 0; col < nCols; ++col) {
				elements.get(row).getStrings()[col].addObserver(cellObserver);
			}
		}
	}

	public void ancestorRemoved(AncestorEvent event) {
		elements.removeObserver(this);
		for (int row=0;row<nRows;++row) {
			for (int col = 0; col < nCols; ++col) {
				elements.get(row).getStrings()[col].removeObserver(cellObserver);
			}
		}
	}

	public void ancestorMoved(AncestorEvent event) {}

	public class RowPanel extends JPanel implements ActionListener {
		int					row;
		final LineField[]	fields;
		final CustomButton	add;
		final CustomButton	remove;
		final CustomButton	up;
		final CustomButton	down;

		RowPanel(int row) {
			super(null);
			this.row = row;
			fields = new LineField[nCols];
			for (int i = 0; i < nCols; ++i) {
				fields[i] = new LineField(elements.get(row).getStrings()[i],getCaretRef(),true);
				fields[i].setBorder(TEXT_FIELD_BORDER);
				add(fields[i]);
			}
			add = new ImageButton(ADD_ICONS[0],ADD_ICONS[1],ADD_ICONS[2],ADD_ICONS[3]);
			add.addActionListener(this);
			add(add);
			remove = new ImageButton(REMOVE_ICONS[0],REMOVE_ICONS[1],REMOVE_ICONS[2],REMOVE_ICONS[3]);
			remove.addActionListener(this);
			add(remove);
			up = new ImageButton(UP_ICONS[0],UP_ICONS[1],UP_ICONS[2],UP_ICONS[3],row!=0);
			up.addActionListener(this);
			add(up);
			down = new ImageButton(DOWN_ICONS[0],DOWN_ICONS[1],DOWN_ICONS[2],DOWN_ICONS[3],row!=nRows-1);
			down.addActionListener(this);
			add(down);
		}

		public int getRow() {
			return row;
		}
		public int getCol(LineField field) {
			for (int col=0;col<fields.length;++col) {
				if (fields[col]==field) {
					return col;
				}
			}
			return -1;
		}
		public int getNumCols() {
			return fields.length;
		}
		public LineField getField(int col) {
			return fields[col];
		}

		public void actionPerformed(ActionEvent e) {
			try {
				if (e.getSource()==add) {
					addRow(row+1,true);
				}
				else if (e.getSource()==remove) {
					removeRow(row,true);
				}
				else if (e.getSource()==up) {
					moveRowUp(row,true);
				}
				else if (e.getSource()==down) {
					moveRowDown(row,true);
				}
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while adding/removing/moving a table row.\nIt should be easy to fix if you submit a bug report on http://forum.codecortex.com/\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
		public void updateRow(int newRow) {
			row = newRow;
			up.setEnabled(row!=0);
			down.setEnabled(row!=nRows-1);
		}

		public void setBackground(Color bg) {
			super.setBackground(bg);
			setOpaque(bg.getAlpha()==255);
			if (add!=null) {
				add.setBackground(bg);
			}
			if (remove!=null) {
				remove.setBackground(bg);
			}
			if (up!=null) {
				up.setBackground(bg);
			}
			if (down!=null) {
				down.setBackground(bg);
			}
			if (fields!=null) {
				for (LineField field : fields) {
					field.setBackground(bg);
				}
			}
		}

		public void setForeground(Color fg) {
			super.setForeground(fg);
			if (add!=null) {
				add.setForeground(fg);
			}
			if (remove!=null) {
				remove.setForeground(fg);
			}
			if (up!=null) {
				up.setForeground(fg);
			}
			if (down!=null) {
				down.setForeground(fg);
			}
			if (fields!=null) {
				for (LineField field : fields) {
					field.setForeground(fg);
				}
			}
		}

		public void setFont(Font font) {
			super.setFont(font);
			if (fields!=null) {
				for (LineField field : fields) {
					field.setFont(font);
				}
				// NOTE: Do not redo the layout here to avoid redoing the layout for every row.
			}
		}
	}
}
